/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.codegen.core;

import com.querydsl.codegen.EntityType;
import com.querydsl.codegen.Property;
import com.querydsl.codegen.Serializer;
import com.querydsl.codegen.SerializerConfig;
import com.querydsl.codegen.Supertype;
import com.querydsl.codegen.utils.CodeWriter;
import com.querydsl.codegen.utils.model.ClassType;
import com.querydsl.codegen.utils.model.Parameter;
import com.querydsl.codegen.utils.model.Type;
import com.querydsl.codegen.utils.model.Types;
import com.querydsl.core.util.BeanUtils;
import com.querydsl.sql.ColumnMetadata;
import com.querydsl.sql.codegen.support.PrimaryKeyData;

import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

/**
 * @author <a href=mailto:hedyn@foxmail.com>HeDYn</a>
 * @see com.querydsl.codegen.BeanSerializer
 */
public class JavabeanSerializer implements Serializer {

    private static final Function<Property, Parameter> PROPERTY_TO_PARAMETER
            = input -> new Parameter(input.getName(), input.getType());

    private final boolean propertyAnnotations;
    private final List<Type> interfaces = new ArrayList<>();
    private final String javadocSuffix;

    private boolean addFullConstructor = false;
    private boolean addToString = true;
    private boolean addCloneFrom = true;
    private boolean printSupertype = false;
    private Comparator<Property> columnComparator;

    public JavabeanSerializer() {
        this(true, " is a Querydsl bean type");
    }

    public JavabeanSerializer(String javadocSuffix) {
        this(true, javadocSuffix);
    }

    public JavabeanSerializer(boolean propertyAnnotations) {
        this(propertyAnnotations, " is a Querydsl bean type");
    }

    public JavabeanSerializer(boolean propertyAnnotations, String javadocSuffix) {
        this.propertyAnnotations = propertyAnnotations;
        this.javadocSuffix = javadocSuffix;
        setPrintSupertype(true);
    }

    @Override
    public void serialize(EntityType model, SerializerConfig serializerConfig, CodeWriter writer) throws IOException {
        String simpleName = model.getSimpleName();

        // package
        if (!model.getPackageName().isEmpty()) {
            writer.packageDecl(model.getPackageName());
        }

        // imports
        Set<String> importedClasses = getAnnotationTypes(model);
        for (Type iface : interfaces) {
            importedClasses.add(iface.getFullName());
        }
        //importedClasses.add(Generated.class.getName());
        if (model.hasLists()) {
            importedClasses.add(List.class.getName());
        }
        if (model.hasCollections()) {
            importedClasses.add(Collection.class.getName());
        }
        if (model.hasSets()) {
            importedClasses.add(Set.class.getName());
        }
        if (model.hasMaps()) {
            importedClasses.add(Map.class.getName());
        }
        if (addToString && model.hasArrays()) {
            importedClasses.add(Arrays.class.getName());
        }
        writer.importClasses(importedClasses.toArray(new String[0]));

        // javadoc
        writer.javadoc(simpleName + javadocSuffix
                + ". the database table \""
                + model.getData().get("table") + "\"");

        // header
        for (Annotation annotation : model.getAnnotations()) {
            writer.annotation(annotation);
        }

        //writer.line("@Generated(\"", getClass().getName(), "\")");

        if (!interfaces.isEmpty()) {
            Type superType = null;
            if (printSupertype && model.getSuperType() != null) {
                superType = model.getSuperType().getType();
            }
            Type[] ifaces = interfaces.toArray(new Type[0]);
            writer.beginClass(model, superType, ifaces);
        } else if (printSupertype && model.getSuperType() != null) {
            writer.beginClass(model, model.getSuperType().getType());
        } else {
            writer.beginClass(model);
        }


        bodyStart(model, writer);

        if (addFullConstructor) {
            addFullConstructor(model, writer);
        }

        List<Property> properties = CodegenUtils.getProperties(model, columnComparator);
        serializeProperties(model, properties, writer);

        if (addCloneFrom) {
            addCloneFrom(model, properties, writer);
        }
        if (addToString) {
            addToString(properties, writer);
        }
        addHashCodeAndEquals(model, writer);

        bodyEnd(model, writer);

        writer.end();
    }

    protected void serializeProperties(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        // fields
        for (Property property : properties) {
            if (propertyAnnotations) {
                for (Annotation annotation : property.getAnnotations()) {
                    writer.annotation(annotation);
                }
            }
            ColumnMetadata metadata = (ColumnMetadata) property.getData().get("COLUMN");
            writer.javadoc(metadata.getName());
            writer.privateField(property.getType(), property.getEscapedName());
        }

        // accessors
        for (Property property : properties) {
            String propertyName = property.getEscapedName();
            // getter
            writer.beginPublicMethod(property.getType(), "get" + BeanUtils.capitalize(propertyName));
            writer.line("return ", propertyName, ";");
            writer.end();
            // setter
            Parameter parameter = new Parameter(propertyName, property.getType());
            writer.beginPublicMethod(Types.VOID, "set" + BeanUtils.capitalize(propertyName), parameter);
            writer.line("this.", propertyName, " = ", propertyName, ";");
            writer.end();
        }
    }

    protected void addFullConstructor(EntityType model, CodeWriter writer) throws IOException {
        // public empty constructor
        writer.beginConstructor();
        writer.end();

        // full constructor
        writer.beginConstructor(model.getProperties(), PROPERTY_TO_PARAMETER);
        for (Property property : model.getProperties()) {
            writer.line("this.", property.getEscapedName(), " = ", property.getEscapedName(), ";");
        }
        writer.end();
    }

    protected void addHashCodeAndEquals(EntityType model, CodeWriter writer) throws IOException {
        if (model.getSuperType() != null) {
            // hashCode
            writer.line("@Override");
            writer.beginPublicMethod(Types.INT, "hashCode");
            writer.line("return super.hashCode();");
            writer.end();

            // equals
            Parameter parameter = new Parameter("o", new ClassType(Object.class));
            writer.line("@Override");
            writer.beginPublicMethod(Types.BOOLEAN_P, "equals", parameter);
            writer.line("return super.equals(o);");
            writer.end();
        } else {
            Map<String, Property> cloumnPropertyMap = new HashMap<>();
            for (Property property : model.getProperties()) {
                ColumnMetadata column = (ColumnMetadata) property.getData().get("COLUMN");
                cloumnPropertyMap.put(column.getName(), property);
            }

            // primary keys
            List<Property> pkPropertyList = new ArrayList<>();
            @SuppressWarnings("unchecked")
            PrimaryKeyData primaryKey = (PrimaryKeyData) model.getData().get(PrimaryKeyData.class);
            if (primaryKey != null) {
                for (String column : primaryKey.getColumns()) {
                    Property property = cloumnPropertyMap.get(column);
                    pkPropertyList.add(property);
                }
            }

            if (pkPropertyList.size() == 1) {
                Property firstProperty = pkPropertyList.remove(0);
                // hashCode
                writer.line("@Override");
                writer.beginPublicMethod(Types.INT, "hashCode");
                writer.line("return java.util.Objects.hashCode(", firstProperty.getEscapedName(), ");");
                writer.end();

                // equals
                Parameter parameter = new Parameter("o", new ClassType(Object.class));
                writer.line("@Override");
                writer.beginPublicMethod(Types.BOOLEAN_P, "equals", parameter);
                writer.line("if (o == null || getClass() != o.getClass()) return false;");
                writer.line(model.getSimpleName(), " that = (", model.getSimpleName(), ") o;");
                writer.line("return java.util.Objects.equals(", firstProperty.getEscapedName(), ", that.", firstProperty.getEscapedName(), ");");
                writer.end();
            } else if (pkPropertyList.size() > 1) {
                Property firstProperty = pkPropertyList.remove(0);
                Property lastProperty = pkPropertyList.remove(pkPropertyList.size() - 1);
                // hashCode
                writer.line("@Override");
                writer.beginPublicMethod(Types.INT, "hashCode");
                writer.line("int result = java.util.Objects.hashCode(", firstProperty.getEscapedName(), ");");
                for (Property property : pkPropertyList) {
                    writer.line("result = 31 * result + java.util.Objects.hashCode(", property.getEscapedName(), ");");
                }
                writer.line("return 31 * result + java.util.Objects.hashCode(", lastProperty.getEscapedName(), ");");
                writer.end();

                // equals
                Parameter parameter = new Parameter("o", new ClassType(Object.class));
                writer.line("@Override");
                writer.beginPublicMethod(Types.BOOLEAN_P, "equals", parameter);
                writer.line("if (o == null || getClass() != o.getClass()) return false;");
                writer.line(model.getSimpleName(), " that = (", model.getSimpleName(), ") o;");
                writer.line("if (!java.util.Objects.equals(", firstProperty.getEscapedName(), ", that.", firstProperty.getEscapedName(),")) return false;");
                for (Property property : pkPropertyList) {
                    writer.line("if (!java.util.Objects.equals(", property.getEscapedName(), ", that.", property.getEscapedName(),")) return false;");
                }
                writer.line("return java.util.Objects.equals(", lastProperty.getEscapedName(), ", that.", lastProperty.getEscapedName(),");");
                writer.end();
            }
        }
    }

    protected void addToString(List<Property> properties, CodeWriter writer) throws IOException {
        if (!properties.isEmpty()) {
            writer.line("@Override");
            writer.beginPublicMethod(Types.STRING, "toString");
            writer.line("StringBuilder sb = new StringBuilder();");
            writer.line("sb.append(super.toString());");
            for (Property property : properties) {
                String propertyName = property.getEscapedName();
                Class<?> classType = property.getType().getJavaClass();
                if (!(byte[].class.isAssignableFrom(classType))) {
                    writer.line("sb.append(\",", propertyName, "=\").append(", propertyName, ");");
                }
            }
            writer.line("return sb.toString();");
            writer.end();
        }
    }

    protected void addCloneFrom(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        Parameter parameter = new Parameter("from", model);
        writer.beginPublicMethod(Types.VOID, "cloneFrom", parameter);

        Supertype supertype = model.getSuperType();
        if (supertype != null) {
            try {
                Class<?> superclass = supertype.getType().getJavaClass();
                boolean superHasCloneFrom = CodegenUtils.isSuperMethod(superclass, "cloneFrom");
                if (superHasCloneFrom) {
                    writer.line("super.cloneFrom(from);");
                }
            } catch (Exception ignored) {
            }
        }

        for (Property property : properties) {
            String propertyName = property.getEscapedName();
            writer.line("this.", propertyName, " = from.", propertyName, ";");
        }
        writer.end();
    }

    protected void bodyStart(EntityType model, CodeWriter writer) throws IOException {
        com.querydsl.codegen.Supertype supertype = model.getSuperType();
        if (supertype != null) {
            Class<?> superClass = supertype.getType().getJavaClass();
            if (Serializable.class.isAssignableFrom(superClass)) {
                writer.privateStaticFinal(Types.LONG_P, "serialVersionUID", "1L");
            }
        }
    }

    protected void bodyEnd(EntityType model, CodeWriter writer) throws IOException {
    }

    private Set<String> getAnnotationTypes(EntityType model) {
        Set<String> imports = new HashSet<>();
        for (Annotation annotation : model.getAnnotations()) {
            imports.add(annotation.annotationType().getName());
        }
        if (propertyAnnotations) {
            for (Property property : model.getProperties()) {
                for (Annotation annotation : property.getAnnotations()) {
                    imports.add(annotation.annotationType().getName());
                }
            }
        }
        return imports;
    }

    public void addInterface(Class<?> iface) {
        interfaces.add(new ClassType(iface));
    }

    public void addInterface(Type type) {
        interfaces.add(type);
    }

    public void setAddToString(boolean addToString) {
        this.addToString = addToString;
    }

    public void setAddFullConstructor(boolean addFullConstructor) {
        this.addFullConstructor = addFullConstructor;
    }

    public void setAddCloneFrom(boolean addCloneFrom) {
        this.addCloneFrom = addCloneFrom;
    }

    public void setPrintSupertype(boolean printSupertype) {
        this.printSupertype = printSupertype;
    }

    public void setColumnComparator(Comparator<Property> columnComparator) {
        this.columnComparator = columnComparator;
    }
}
